/*
 * (c) 2012 Leonid Bensman, lbensman.cs00@gtalumni.org
 */
using System;
using System.Collections.Generic;
using System.Text;

using System.IO;
using System.Security.Cryptography;
using Yeti.WMFSdk;
using System.Threading;
using CalcSig.Serializers;

namespace CalcSig
{
    public struct JobStats
    {
        public int Records;
        public long Bytes;

        public void AddRecordToEstimates()
        {
            Interlocked.Increment(ref this.Records);
        }

        public void AddRecordToProgress()
        {
            Interlocked.Increment(ref this.Records);
        }

        public void AddFileToEstimates(FileInfo fileInfo)
        {
            Interlocked.Add(ref this.Bytes, fileInfo.Length);
            Interlocked.Increment(ref this.Records);
        }

        public void AddFileToProgress(ItemHash itemHash)
        {
            Interlocked.Add(ref this.Bytes, itemHash.FileSize);
            Interlocked.Increment(ref this.Records);
        }

        public void DisplayProgress(JobStats inProgressStats)
        {
            if (this.Records < inProgressStats.Records || (this.Records == inProgressStats.Records && this.Bytes < inProgressStats.Bytes))
            {
                inProgressStats.DisplayProgress(this);
            }
            else
            {
                Console.WriteLine(string.Format(@"Files: {0}/{1} ({2:P}), Bytes {3}/{4} ({5:P})",
                    inProgressStats.Records, this.Records, (double)inProgressStats.Records / (double)this.Records,
                    inProgressStats.Bytes, this.Bytes, (double)inProgressStats.Bytes / (double)this.Bytes
                    ));
            }
        }
    }

    public class CLIDirectives
    {
        public enum HashCalculationType
        {
            Unspecified,
            Audio,
            Image,
            Video,
            PHashDistance
        }

        private HashCalculationType _hashType = HashCalculationType.Unspecified;
        public HashCalculationType HashType 
        { 
            get
            {
                return _hashType;
            }
            private set
            {
                _hashType = value;
            }
        }

        public enum HashSerializationFormats
        {
            CSV            
        }

        private HashSerializationFormats _hashSerializationFormat;
        public HashSerializationFormats HashSerializationFormat 
        {
            get
            {
                return _hashSerializationFormat;
            }
            private set
            {
                _hashSerializationFormat = value;
            }
        }

        private string[] _searchTargets = new string[0];
        public string[] SearchTargets 
        {
            get
            {
                return _searchTargets;
            }
            private set
            {
                _searchTargets = value;
            }
        }

        private string[] _excludeSearchMask = new string[0];
        public string[] ExcludeSearchMask
        {
            get
            {
                return _excludeSearchMask;
            }
            private set
            {
                _excludeSearchMask = value;
            }
        }

        public int? DistanceFilter { get; set; }

        private bool _recurse = true;
        public bool Recurse
        {
            get
            {
                return _recurse;
            }
            private set
            {
                _recurse = value;
            }
        }

        private CLIDirectives()
        {
            // Default serialization format to CSV.
            this.HashSerializationFormat = HashSerializationFormats.CSV;
        }

        private static CLIDirectives _instace;
        public static CLIDirectives Instance
        {
            get
            {
                if (_instace == null)
                {
                    _instace = ParseCLI();
                }
                return _instace;
            }
        }

        private static CLIDirectives ParseCLI()
        {
            CLIDirectives cliDirectives = new CLIDirectives();

            string[] cliArgs = Environment.GetCommandLineArgs();

            if (cliArgs.Length <= 1)
            {
                PrintHelp();
                return null;
            }

            for (int argNumber = 1; argNumber < cliArgs.Length; argNumber++ )
            {
                string currentArg = cliArgs[argNumber];
                if (currentArg == "/?" || currentArg == "-?" || currentArg == "-h")
                {
                    PrintHelp();
                    return null;
                }
                else if (currentArg == "/AUDIO" || currentArg == "/MUSIC")
                {
                    cliDirectives.HashType = HashCalculationType.Audio;
                }
                else if (currentArg == "/PICTURE" || currentArg == "/PHOTO" || currentArg == "/IMAGE")
                {
                    cliDirectives.HashType = HashCalculationType.Image;
                }
                else if (currentArg == "/VIDEO")
                {
                    cliDirectives.HashType = HashCalculationType.Video;
                }
                else if (currentArg == "/PHASH")
                {
                    cliDirectives.HashType = HashCalculationType.PHashDistance;
                }
                else if (currentArg.StartsWith("/FORMAT:"))
                {
                    string formatType = currentArg.Substring("/FORMAT:".Length);

                    if (formatType == "CSV")
                    {
                        cliDirectives.HashSerializationFormat = HashSerializationFormats.CSV;
                    }
                    else
                    {
                        throw new ArgumentException(string.Format("Unknown output format specifier - {0}.", formatType), "/FORMAT");
                    }
                }
                else if (currentArg.StartsWith("/EXCLUDE:"))
                {
                    string excludeString = currentArg.Substring("/EXCLUDE:".Length);

                    if (cliDirectives.ExcludeSearchMask != null)
                    {
                        string[] newExcludeSearchMask = new string[cliDirectives.ExcludeSearchMask.Length + 1];
                        cliDirectives.ExcludeSearchMask.CopyTo(newExcludeSearchMask, 0);
                        newExcludeSearchMask.SetValue(excludeString, newExcludeSearchMask.Length - 1);
                        cliDirectives.ExcludeSearchMask = newExcludeSearchMask;
                    }
                    else
                    {
                        cliDirectives.ExcludeSearchMask = new string[] { excludeString };
                    }
                }
                else if (currentArg.StartsWith("/MAXDISTANCEFILTER:"))
                {
                    string distanceFilterString = currentArg.Substring("/MAXDISTANCEFILTER:".Length);
                    int distanceFilter;

                    if (int.TryParse(distanceFilterString, out distanceFilter) && distanceFilter >= 0)
                    {
                        cliDirectives.DistanceFilter = distanceFilter;
                    }
                    else
                    {
                        throw new ArgumentException(string.Format("Invalid value specified for max distance. Must be numeric and greater or equal to zero."), "/MAXDISTANCEFILTER:");
                    }
                }
                else if (currentArg == "/-S")
                {
                    cliDirectives.Recurse = false;
                }
                else if (currentArg.StartsWith("/"))
                {
                    throw new ArgumentException(string.Format("Unknown switch specified - {0}.", currentArg), currentArg);
                }
                else
                {
                    //If we get here, we treat this as mask
                    if (cliDirectives.SearchTargets != null)
                    {
                        string[] newSearchMask = new string[cliDirectives.SearchTargets.Length + 1];
                        cliDirectives.SearchTargets.CopyTo(newSearchMask, 0);
                        newSearchMask.SetValue(currentArg, newSearchMask.Length - 1);
                        cliDirectives.SearchTargets = newSearchMask;
                    }
                    else
                    {
                        cliDirectives.SearchTargets = new string[] { currentArg };
                    }
                }
            }

            PreValidateParsedCLI(cliDirectives);

            return cliDirectives;
        }

        private static void PreValidateParsedCLI(CLIDirectives cliDirectives)
        {
            if (cliDirectives.HashType == HashCalculationType.Unspecified)
            {
                throw new ArgumentException("Error: Hash calculation type not specified.  Specify /AUDIO or /IMAGE.  Use /? to see usage.");
            }

            if (cliDirectives.HashType == HashCalculationType.PHashDistance)
            {
                if (cliDirectives.ExcludeSearchMask.Length > 0)
                {
                    throw new ArgumentException("Error: /EXCLUDE is not compatible with /PHASH option");
                }
            }

            if (cliDirectives.SearchTargets.Length == 0)
            {
                cliDirectives.SearchTargets = new string[] { Environment.CurrentDirectory + Path.DirectorySeparatorChar };
            }
        }

        private static void PrintHelp()
        {
            Console.WriteLine("Calculates comparison hashes for audio and picture files to help identify identical or similar content files.");
            Console.WriteLine("(C)2012 by Leonid Bensman (lbensman.cs00@gtalumni.org), All Rights Reserved.");
            Console.WriteLine();
            Console.WriteLine("Usage: CalcSig.exe [/?|-?|-h]");
            Console.WriteLine("Usage: CalcSig.exe [/AUDIO | /MUSIC] | [/IMAGE | /PHOTO | /PICTURE] [/EXCLUDE:excludeSearchMask] [/FORMAT:format] [/-S] [searchMask [searchMask [...]]");
            Console.WriteLine("Usage: CalcSig.exe /PHASH [/FORMAT:format] [/MAXDISTANCEFILTER:maxDistance] sourceHashes");
            Console.WriteLine("  /AUDIO\tCalculates hashes for audio files.");
            Console.WriteLine("  /MUSIC\tSame as /AUDIO.");
            Console.WriteLine("  /IMAGE\tCalculates hashes for image files.");
            Console.WriteLine("  /PICTURE\tSame as /IMAGE.");
            Console.WriteLine("  /PHOTO\tSame as /IMAGE.");
            Console.WriteLine("  /VIDEO\tCalculates hashes for video files (rudimentary file comparison).");
            Console.WriteLine("  /FORMAT\tSpecifies output format.");
            Console.WriteLine("  format\tCurrently only CSV format is supported and is the default.");
            Console.WriteLine("  /MAXDISTANCEFILTER\tFilters PHASH results so that distance doesn't exceed the value specified.");
            Console.WriteLine("  /-S\t\tDo not recurse into subfolders. Process only current folder.");
            Console.WriteLine("  searchMask\tWildcard support search mask of files to process.");
            Console.WriteLine("  /EXCLUDE\tSpecifies exclusion search mask to exclude files even if they're matched by searchMask.");
            Console.WriteLine("  excludeSearchMask\tWildcard support search mask of files to exlude from those matching searchMask.");
            Console.WriteLine("  /PHASH\tCalculates perceptual hashes distances for precomputed hashes of image files.");
            Console.WriteLine("  \t\tSource file should be one generated with /IMAGE switch and same version of the utility.");
            Console.WriteLine("  sourceHashes\tSource hashes for which to calculate perceptual distances.");
            Console.WriteLine();
            Console.WriteLine("Hash type calculation switch (e.g. /AUDIO or /IMAGE) is required.");
            Console.WriteLine("Format specifier is optional and currently only CSV is supported.");
            Console.WriteLine("searchMask is optional and defaults to formats supported by respective calculation processors.");
        }
    }

    class CalcSig
    {
        static void Main(string[] args)
        {
            CLIDirectives cliDirectives;
            try
            {
                cliDirectives = CLIDirectives.Instance;
            }
            catch (ArgumentException e)
            {
                Console.Error.WriteLine(e.Message);
                return;
            }

            if (cliDirectives == null)
            {
                return;
            }
            else if (cliDirectives.HashType == CLIDirectives.HashCalculationType.Audio)
            {
                IHashCalculator hashCalculator;
                IHashSerializer hashSerializer = SetUpAudioCalculatorAndSerializers(
                    cliDirectives.SearchTargets,
                    AudioHashCalculator.SupportedFileSearchMasks,
                    cliDirectives.ExcludeSearchMask,
                    cliDirectives.Recurse,
                    out hashCalculator);

                ProcessHashJob(hashCalculator, hashSerializer);
            }
            else if (cliDirectives.HashType == CLIDirectives.HashCalculationType.Image)
            {
                IHashCalculator hashCalculator;
                IHashSerializer hashSerializer = SetUpImageCalculatorAndSerializers(
                    cliDirectives.SearchTargets,
                    ImagePHashAverageCalculator.SupportedFileSearchMasks,
                    cliDirectives.ExcludeSearchMask,
                    cliDirectives.Recurse,
                    out hashCalculator);

                ProcessHashJob(hashCalculator, hashSerializer);
            }
            else if (cliDirectives.HashType == CLIDirectives.HashCalculationType.Video)
            {
                IHashCalculator hashCalculator;
                IHashSerializer hashSerializer = SetUpVideoCalculatorAndSerializers(
                    cliDirectives.SearchTargets,
                    VideoHashCalculator.SupportedFileSearchMasks,
                    cliDirectives.ExcludeSearchMask,
                    cliDirectives.Recurse,
                    out hashCalculator);

                ProcessHashJob(hashCalculator, hashSerializer);
            }
            else if (cliDirectives.HashType == CLIDirectives.HashCalculationType.PHashDistance)
            {
                IHashCalculator hashCalculator;
                List<IDisposable> toDispose;
                IHashSerializer hashSerializer = SetUpPHashDistanceCalculatorAndSerializers(
                    cliDirectives.SearchTargets[0], 
                    cliDirectives.DistanceFilter,
                    out hashCalculator,
                    out toDispose);

                ProcessHashJob(hashCalculator, hashSerializer);
                foreach (IDisposable disposable in toDispose)
                {
                    disposable.Dispose();
                }
            }
            else
            {
                Console.Error.WriteLine("Error understanding CLI arguments - job specified is not known.  Exiting...");
                return;
            }
        }

        private static void ProcessHashJob(IHashCalculator hashCalculator, IHashSerializer hashSerializer)
        {
            Console.WriteLine("Estimating...");
            JobStats jobStats = hashCalculator.Estimate();

            JobStats inProgressJobStats = new JobStats();

            Console.WriteLine("Calculating hashes...");

            try
            {
                hashCalculator.ItemCalculated += delegate(ItemHash itemHash)
                {
                    //if (!(itemHash is AudioHash))
                    //{
                    //    throw new ArgumentException(string.Format("itemHash argument is not derived type {0} as expected, but rather of type {1}", typeof(AudioHash).ToString(), itemHash.GetType().ToString()));
                    //}

                    //AudioHash audioHash = itemHash as AudioHash;

                    inProgressJobStats.AddFileToProgress(itemHash);

                    jobStats.DisplayProgress(inProgressJobStats);
                };

                hashCalculator.CalculateHashes();
            }
            finally
            {
                if (hashSerializer is IDisposable)
                {
                    (hashSerializer as IDisposable).Dispose();
                }
            }
        }

        private static IHashSerializer SetUpAudioCalculatorAndSerializers(IEnumerable<string> masks, IEnumerable<string> defaultSearchMasks, IEnumerable<string> excludeMasks, bool recursive, out IHashCalculator audioCalculator)
        {
            DirectoryEnumerator fileEnumerator = new DirectoryEnumerator(masks, defaultSearchMasks, excludeMasks, recursive);

            audioCalculator = new AudioHashCalculator(fileEnumerator);

            AudioHashSerializerCSV audioSerializer = new AudioHashSerializerCSV(audioCalculator);

            return audioSerializer;
        }

        private static IHashSerializer SetUpImageCalculatorAndSerializers(IEnumerable<string> masks, IEnumerable<string> defaultSearchMasks, IEnumerable<string> excludeMasks, bool recursive, out IHashCalculator imageCalculator)
        {
            DirectoryEnumerator fileEnumerator = new DirectoryEnumerator(masks, defaultSearchMasks, excludeMasks, recursive);

            imageCalculator = new ImagePHashAverageCalculator(fileEnumerator);

            ImageHashSerializerCSV imageSerializer = new ImageHashSerializerCSV(imageCalculator);

            return imageSerializer;
        }

        private static IHashSerializer SetUpVideoCalculatorAndSerializers(IEnumerable<string> masks, IEnumerable<string> defaultSearchMasks, IEnumerable<string> excludeMasks, bool recursive, out IHashCalculator imageCalculator)
        {
            DirectoryEnumerator fileEnumerator = new DirectoryEnumerator(masks, defaultSearchMasks, excludeMasks, recursive);

            imageCalculator = new VideoHashCalculator(fileEnumerator);

            var imageSerializer = new VideoHashSerializerCSV(imageCalculator);

            return imageSerializer;
        }

        private static IHashSerializer SetUpPHashDistanceCalculatorAndSerializers(string sourceImageHashesFile, int? distanceFilter, out IHashCalculator imageCalculator, out List<IDisposable> toDisposeOnCompletion)
        {
            toDisposeOnCompletion = new List<IDisposable>(2);

            FileStream imageHashesStream = File.OpenRead(sourceImageHashesFile);
            toDisposeOnCompletion.Add(imageHashesStream);
            StreamReader imageHashesStreamReader = new StreamReader(imageHashesStream);
            toDisposeOnCompletion.Add(imageHashesStreamReader);

            ImageHashDeserializerCSV recordDeserializer = new ImageHashDeserializerCSV(imageHashesStreamReader);
            IEnumerable<ImagePHash> recordEnumerable = recordDeserializer.Deserialize();

            imageCalculator = new ImagePHashDistanceCalculator(recordEnumerable);

            if (distanceFilter.HasValue)
            {
                imageCalculator = new ImagePHashDistanceFilterCalculator(imageCalculator, distanceFilter.Value);
            }

            ImagePHashDistanceSerializerCSV imageSerializer = new ImagePHashDistanceSerializerCSV(imageCalculator);

            return imageSerializer;
        }
    }
}
